點擊進入React源碼調試倉庫。
每個fiber節點在更新時都會經歷兩個階段:beginWork和completeWork。在Diff之後(詳見深入理解React Diff原理),workInProgress節點就會進入complete階段。這個時候拿到的workInProgress節點都是經過diff算法調和過的,也就意味著對於某個節點來說它fiber的形態已經基本確定了,但除此之外還有兩點:
基於這兩個特點,completeWork的工作主要有:
對於正常執行工作的workInProgress節點來說,會走以上的流程。但是免不了節點的更新會出錯,所以對出錯的節點會采取措施,這涉及到錯誤邊界以及Suspense的概念,
本文只做簡單流程分析。
這壹節涉及的知識點有
completeUnitOfWork是completeWork階段的入口。它內部有壹個循環,會自下而上地遍歷workInProgress節點,依次處理節點。
對於正常的workInProgress節點,會執行completeWork。這其中會對HostComponent組件完成更新props、綁定事件等DOM相關的工作。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.effectTag & Incomplete) === NoEffect) {
// 如果workInProgress節點沒有出錯,走正常的complete流程
...
let next;
// 省略了判斷邏輯
// 對節點進行completeWork,生成DOM,更新props,綁定事件
next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
// 任務被掛起的情況,
workInProgress = next;
return;
}
// 收集workInProgress節點的lanes,不漏掉被跳過的update的lanes,便於再次發起調度
resetChildLanes(completedWork);
// 將當前節點的effectList並入父級節點
...
// 如果當前節點他自己也有effectTag,將它自己
// 也並入到父級節點的effectList
} else {
// 執行到這個分支說明之前的更新有錯誤
// 進入unwindWork
const next = unwindWork(completedWork, subtreeRenderLanes);
...
}
// 查找兄弟節點,若有則進行beginWork -> completeWork
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
// 若沒有兄弟節點,那麽向上回到父級節點
// 父節點進入complete
completedWork = returnFiber;
// 將workInProgress節點指向父級節點
workInProgress = completedWork;
} while (completedWork !== null);
// 到達了root,整棵樹完成了工作,標記完成狀態
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
由於React的大部分的fiber節點最終都要體現為DOM,所以本文主要分析HostComponent相關的處理流程。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
switch (workInProgress.tag) {
...
case HostComponent: {
...
if (current !== null && workInProgress.stateNode != null) {
// 更新
} else {
// 創建
}
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
// 更新
} else {
// 創建
}
return null;
}
case SuspenseComponent:
...
}
}
由completeWork的結構可以看出,就是依據fiber的tag做不同處理。對HostComponent 和 HostText的處理是類似的,都是針對它們的DOM節點,處理方法又會分為更新和創建。
若current存在並且workInProgress.stateNode(workInProgress節點對應的DOM實例)存在,說明此workInProgress節點的DOM節點已經存在,走更新邏輯,否則進行創建。
DOM節點的更新實則是屬性的更新,會在下面的DOM屬性的處理 -> 屬性的更新
中講到,先來看壹下DOM節點的創建和插入。
我們知道,此時的completeWork處理的是經過diff算法之後產生的新fiber。對於HostComponent類型的新fiber來說,它可能有DOM節點,也可能沒有。沒有的話,
就需要執行先創建,再插入的操作,由此引入DOM的插入算法。
if (current !== null && workInProgress.stateNode != null) {
// 表明fiber有dom節點,需要執行更新過程
} else {
// fiber不存在DOM節點
// 先創建DOM節點
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
//DOM節點插入
appendAllChildren(instance, workInProgress, false, false);
// 將DOM節點掛載到fiber的stateNode上
workInProgress.stateNode = instance;
...
}
需要註意的是,DOM的插入並不是將當前DOM插入它的父節點,而是將當前這個DOM節點的第壹層子節點插入到它自己的下面。
此時的completeWork階段,會自下而上遍歷workInProgress樹到root,每經過壹層都會按照上面的規則插入DOM。下邊用壹個例子來理解壹下這個過程。
這是壹棵fiber樹的結構,workInProgress樹最終要成為這個形態。
1 App
|
|
2 div
/
/
3 <List/>--->span
/
/
4 p ----> 'text node'
/
/
5 h1
構建workInProgress樹的DFS遍歷對沿途節點壹路beginWork,此時已經遍歷到最深的h1節點,它的beginWork已經結束,開始進入completeWork階段,此時所在的層級深度為第5層。
第5層
1 App
|
|
2 div
/
/
3 <List/>
/
/
4 p
/
/
5--->h1
此時workInProgress節點指向h1的fiber,它對應的dom節點為h1,dom標簽創建出來以後進入appendAllChildren
,因為當前的workInProgress節點為h1,所以它的child為null,無子節點可插入,退出。
h1節點完成工作往上返回到第4層的p節點。
此時的dom樹為
h1
第4層
1 App
|
|
2 div
/
/
3 <List/>
/
/
4 ---> p ----> 'text node'
/
/
5 h1
此時workInProgress節點指向p的fiber,它對應的dom節點為p,進入appendAllChildren
,發現 p 的child為 h1,並且是HostComponent組件,將 h1 插入 p,然後尋找子節點h1是否有同級的sibling節點。發現沒有,退出。
p節點的所有工作完成,它的兄弟節點:HostText類型的組件'text'會作為下壹個工作單元,執行beginWork再進入completeWork。現在需要對它執行appendAllChildren
,發現沒有child,不執行插入操作。它的工作也完成,return到父節點<List/>
,進入第3層
此時的dom樹為
p 'text'
/
/
h1
第3層
1 App
|
|
2 div
/
/
3 ---> <List/>--->span
/
/
4 p ----> 'text'
/
/
5 h1
此時workInProgress節點指向<List/>
的fiber,對它進行completeWork,由於此時它是自定義組件,不屬於HostComponent,所以不會對它進行子節點的插入操作。
尋找它的兄弟節點span,對span先進行beginWork再進行到completeWork,執行span子節點的插入操作,發現它沒有child,退出。return到父節點div,進入第二層。
此時的dom樹為
span
p 'text'
/
/
h1
第2層
1 App
|
|
2 ---------> div
/
/
3 <List/>--->span
/
/
4 p ---->'text'
/
/
5 h1
此時workInProgress節點指向div的fiber,對它進行completeWork,執行div的子節點插入。由於它的child是,不滿足node.tag === HostComponent || node.tag === HostText
的條件,所以不會將它插入到div中。繼續向下找的child,發現是p,將P插入div,然後尋找p的sibling,發現了'text',將它也插入div。之後再也找不到同級節點,此時回到第三層的節點。
有sibling節點span,將span插入到div。由於span沒有子節點,退出。
此時的dom樹為
div
/ | \
/ | \
p 'text' span
/
/
h1
第1層
此時workInProgress節點指向App的fiber,由於它是自定義節點,所以不會對它進行子節點的插入操作。
到此為止,dom樹基本構建完成。在這個過程中我們可以總結出幾個規律:
向節點中插入dom節點時,只插入它子節點中第壹層的dom。可以把這個插入可以看成是壹個自下而上收集dom節點的過程。第壹層子節點之下的dom,已經在第壹層子節點執行插入時被插入第壹層子節點了,從下往上逐層completeWork
的這個過程類似於dom節點的累加。
總是優先看本身可否插入,再往下找,之後才是找sibling節點。
這是由於fiber樹和dom樹的差異導致,每個fiber節點不壹定對應壹個dom節點,但壹個dom節點壹定對應壹個fiber節點。
fiber樹 DOM樹
<App/> div
| |
div input
|
<Input/>
|
input
由於壹個原生DOM組件的子組件有可能是類組件或函數組件,所以會優先檢查自身,發現自己不是原生DOM組件,不能被插入到父級fiber節點對應的DOM中,所以要往下找,直到找到原生DOM組件,執行插入,最後再從這壹層找同級的fiber節點,同級節點也會執行先自檢,再檢查下級,再檢查下級的同級
的操作。
可以看出,節點的插入也是深度優先。值得註意的是,這壹整個插入的流程並沒有真的將DOM插入到真實的頁面上,它只是在操作fiber上的stateNode。真實的插入DOM操作發生在commit階段。
下面是插入節點算法的源碼,可以對照上面的過程來看。
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// 找到當前節點的子fiber節點
let node = workInProgress.child;
// 當存在子節點時,去往下遍歷
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
// 子節點是原生DOM 節點,直接可以插入
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {
// 如果是HostPortal類型的節點,什麽都不做
} else if (node.child !== null) {
// 代碼執行到這,說明node不符合插入要求,
// 繼續尋找子節點
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
// 當不存在兄弟節點時往上找,此過程發生在當前completeWork節點的子節點再無子節點的場景,
// 並不是直接從當前completeWork的節點去往上找
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
// 當不存在子節點時,從sibling節點入手開始找
node.sibling.return = node.return;
node = node.sibling;
}
};
上面的插入過程完成了DOM樹的構建,這之後要做的就是為每個DOM節點計算它自己的屬性(props)。由於節點存在創建和更新兩種情況,所以對屬性的處理也會區別對待。
屬性的創建相對更新來說比較簡單,這個過程發生在DOM節點構建的最後,調用finalizeInitialChildren
函數完成新節點的屬性設置。
if (current !== null && workInProgress.stateNode != null) {
// 更新
} else {
...
// 創建、插入DOM節點的過程
...
// DOM節點屬性的初始化
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
// 最終會依據textarea的autoFocus屬性
// 來決定是否更新fiber
markUpdate(workInProgress);
}
}
finalizeInitialChildren
最終會調用setInitialProperties
,來完成屬性的設置。過程好理解,主要就是調用setInitialDOMProperties
將屬性直接設置進DOM節點(事件在這個階段綁定)
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// 設置行內樣式
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 設置innerHTML
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
}
...
else if (registrationNameDependencies.hasOwnProperty(propKey)) {
// 綁定事件
if (nextProp != null) {
ensureListeningTo(rootContainerElement, propKey);
}
} else if (nextProp != null) {
// 設置其余屬性
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
若對已有DOM節點進行更新,說明只對屬性進行更新即可,因為節點已經存在,不存在刪除和新增的情況。updateHostComponent
函數負責HostComponent對應DOM節點屬性的更新,代碼不多很好理解。
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const oldProps = current.memoizedProps;
// 新舊props相同,不更新
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// prepareUpdate計算新屬性
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// 最終新屬性被掛載到updateQueue中,供commit階段使用
workInProgress.updateQueue = (updatePayload: any);
if (updatePayload) {
// 標記workInProgress節點有更新
markUpdate(workInProgress);
}
};
可以看出它只做了壹件事,就是計算新的屬性,並掛載到workInProgress節點的updateQueue中,它的形式是以2為單位,index為偶數的是key,為奇數的是value:
[ 'style', { color: 'blue' }, title, '測試標題' ]
這個結果由diffProperties
計算產生,它對比lastProps和nextProps,計算出updatePayload。
舉個例子來說,有如下組件,div上綁定的點擊事件會改變它的props。
class PropsDiff extends React.Component {
state = {
title: '更新前的標題',
color: 'red',
fontSize: 18
}
onClickDiv = () => {
this.setState({
title: '更新後的標題',
color: 'blue'
})
}
render() {
const { color, fontSize, title } = this.state
return <div
className="test"
onClick={this.onClickDiv}
title={title}
style={{color, fontSize}}
{...this.state.color === 'red' && { props: '自定義舊屬性' }}
>
測試div的Props變化
</div>
}
}
lastProps和nextProps分別為
lastProps
{
"className": "test",
"title": "更新前的標題",
"style": { "color": "red", "fontSize": 18},
"props": "自定義舊屬性",
"children": "測試div的Props變化",
"onClick": () => {...}
}
nextProps
{
"className": "test",
"title": "更新後的標題",
"style": { "color":"blue", "fontSize":18 },
"children": "測試div的Props變化",
"onClick": () => {...}
}
它們有變化的是propsKey是style、title、props
,經過diff,最終打印出來的updatePayload為
[
"props", null,
"title", "更新後的標題",
"style", {"color":"blue"}
]
diffProperties
內部的規則可以概括為:
若有某個屬性(propKey),它在
對照這個規則看壹下源碼:
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
...
let propKey;
let styleName;
let styleUpdates = null;
for (propKey in lastProps) {
// 循環lastProps,找出需要標記刪除的propKey
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
// 對propKey來說,如果nextProps也有,或者lastProps沒有,那麽
// 就不需要標記為刪除,跳出本次循環繼續判斷下壹個propKey
continue;
}
if (propKey === STYLE) {
// 刪除style
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
} else if(/*...*/) {
...
// 壹些特定種類的propKey的刪除
} else {
// 將其他種類的propKey標記為刪除
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
// 將新prop添加到updatePayload
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
// 如果nextProps不存在propKey,或者前後的value相同,或者前後的value都為null
// 那麽不需要添加進去,跳出本次循環繼續處理下壹個prop
continue;
}
if (propKey === STYLE) {
/*
* lastProp: { color: 'red' }
* nextProp: { color: 'blue' }
* */
// 如果style在lastProps和nextProps中都有
// 那麽需要刪除lastProps中style的樣式
if (lastProp) {
// 如果lastProps中也有style
// 將style內的樣式屬性設置為空
// styleUpdates = { color: '' }
for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
// 以nextProp的屬性名為key設置新的style的value
// styleUpdates = { color: 'blue' }
for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// 如果lastProps中沒有style,說明新增的
// 屬性全部可放入updatePayload
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
// updatePayload: [ style, null ]
}
styleUpdates = nextProp;
// styleUpdates = { color: 'blue' }
}
} else if (/*...*/) {
...
// 壹些特定種類的propKey的處理
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
// 重新綁定事件
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// 事件重新綁定後,需要賦值updatePayload,使這個節點得以被更新
updatePayload = [];
}
} else if (
typeof nextProp === 'object' &&
nextProp !== null &&
nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
) {
// 服務端渲染相關
nextProp.toString();
} else {
// 將計算好的屬性push到updatePayload
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
// 將style和值push進updatePayload
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
console.log('updatePayload', JSON.stringify(updatePayload));
// [ 'style', { color: 'blue' }, title, '測試標題' ]
return updatePayload;
}
DOM節點屬性的diff為workInProgress節點掛載了帶有新屬性的updateQueue,壹旦節點的updateQueue不為空,它就會被標記上Update的effectTag,commit階段會處理updateQueue。
if (updatePayload) {
markUpdate(workInProgress);
}
經過beginWork和上面對於DOM的操作,有變化的workInProgress節點已經被打上了effectTag。
壹旦workInProgress節點持有了effectTag,說明它需要在commit階段被處理。每個workInProgress節點都有壹個firstEffect和lastEffect,是壹個單向鏈表,來表示它自身以及它的子節點上所有持有effectTag的workInProgress節點。completeWork階段在向上遍歷的過程中也會逐層收集effect鏈,最終收集到root上,供接下來的commit階段使用。
實現上相對簡單,對於某個workInProgress節點來說,先將它已有的effectList並入到父級節點,再判斷它自己有沒有effectTag,有的話也並入到父級節點。
/*
* effectList是壹條單向鏈表,每完成壹個工作單元上的任務,
* 都要將它產生的effect鏈表並入
* 上級工作單元。
* */
// 將當前節點的effectList並入到父節點的effectList
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// 將自身添加到effect鏈,添加時跳過NoWork 和
// PerformedWork的effectTag,因為真正
// 的commit用不到
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
每個節點都會執行這樣的操作,最終當回到root的時候,root上會有壹條完整的effectList,包含了所有需要處理的fiber節點。
completeUnitWork中的錯誤處理是錯誤邊界機制的組成部分。
錯誤邊界是壹種React組件,壹旦類組件中使用了getDerivedStateFromError
或componentDidCatch
,就可以捕獲發生在其子樹中的錯誤,那麽它就是錯誤邊界。
回到源碼中,節點如果在更新的過程中報錯,它就會被打上Incomplete的effectTag,說明節點的更新工作未完成,因此不能執行正常的completeWork,要走另壹個判斷分支進行處理。
if ((completedWork.effectTag & Incomplete) === NoEffect) {
} else {
// 有Incomplete的節點會進入到這個判斷分支進行錯誤處理
}
什麽情況下節點會被標記上Incomplete呢?這還要從最外層的工作循環說起。
concurrent模式的渲染函數:renderRootConcurrent之中在構建workInProgress樹時,使用了try...catch來包裹執行函數,這對處理報錯節點提供了機會。
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
壹旦某個節點執行出錯,會進入handleError
函數處理。該函數中可以獲取到當前出錯的workInProgress節點,除此之外我們暫且不關註其他功能,只需清楚它調用了throwException
。
throwException
會為這個出錯的workInProgress節點打上Incomplete 的 effectTag
,表明未完成,在向上找到可以處理錯誤的節點(即錯誤邊界),添加上ShouldCapture 的 effectTag。另外,創建代表錯誤的update,getDerivedStateFromError
放入payload,componentDidCatch
放入callback。最後這個update入隊節點的updateQueue。
throwException
執行完畢,回到出錯的workInProgress節點,執行completeUnitOfWork
,目的是將錯誤終止到當前的節點,因為它本身都出錯了,再向下渲染沒有意義。
function handleError(root, thrownValue):void {
...
// 給當前出錯的workInProgress節點添加上 Incomplete 的effectTag
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
// 開始對錯誤節點執行completeWork階段
completeUnitOfWork(erroredWork);
...
}
重點:從發生錯誤的節點往上找到錯誤邊界,做記號,記號就是ShouldCapture 的 effectTag。
當這個錯誤節點進入completeUnitOfWork時,因為持有了Incomplete
,所以不會進入正常的complete流程,而是會進入錯誤處理的邏輯。
錯誤處理邏輯做的事情:
unwindWork
。Incomplete
,目的是在父節點執行到completeUnitOfWork的時候,也能被執行unwindWork,進而驗證它是否是錯誤邊界。這裏的重點是unwindWork
會驗證節點是否是錯誤邊界,來看壹下unwindWork的關鍵代碼:
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
switch (workInProgress.tag) {
case ClassComponent: {
...
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
// 刪它上面的ShouldCapture,再打上DidCapture
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
...
default:
return null;
}
}
unwindWork
驗證節點是錯誤邊界的依據就是節點上是否有剛剛throwException
的時候打上的ShouldCapture的effectTag。如果驗證成功,最終會被return出去。return出去之後呢?會被賦值給workInProgress節點,我們往下看壹下錯誤處理的整體邏輯:
if ((completedWork.effectTag & Incomplete) === NoEffect) {
// 正常流程
...
} else {
// 驗證節點是否是錯誤邊界
const next = unwindWork(completedWork, subtreeRenderLanes);
if (next !== null) {
// 如果找到了錯誤邊界,刪除與錯誤處理有關的effectTag,
// 例如ShouldCapture、Incomplete,
// 並將workInProgress指針指向next
next.effectTag &= HostEffectMask;
workInProgress = next;
return;
}
// ...省略了React性能分析相關的代碼
if (returnFiber !== null) {
// 將父Fiber的effect list清除,effectTag標記為Incomplete,
// 便於它的父節點再completeWork的時候被unwindWork
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
...
// 繼續向上completeWork的過程
completedWork = returnFiber;
現在我們要有個認知,壹旦unwindWork識別當前的workInProgress節點為錯誤邊界,那麽現在的workInProgress節點就是這個錯誤邊界。然後會刪除掉與錯誤處理有關的effectTag,DidCapture會被保留下來。
if (next !== null) {
next.effectTag &= HostEffectMask;
workInProgress = next;
return;
}
重點:將workInProgress節點指向錯誤邊界,這樣可以對錯誤邊界重新走更新流程。
這個時候workInProgress節點有值,並且跳出了completeUnitOfWork,那麽繼續最外層的工作循環:
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
此時,workInProgress節點,也就是錯誤邊界,它會再被performUnitOfWork處理,然後進入beginWork、completeWork!
也就是說它會被重新更新壹次。為什麽說再被更新呢?因為構建workInProgress樹的時候,beginWork是從上往下的,當時workInProgress指針指向它的時候,它只執行了beginWork。此時子節點出錯導致向上completeUnitOfWork的時候,發現了他是錯誤邊界,workInProgress又指向了它,所以它會再次進行beginWork。不同的是,這次節點上持有了
DidCapture的effectTag。所以流程上是不壹樣的。
還記得throwException
階段入隊錯誤邊界更新隊列的表示錯誤的update嗎?它在此次beginWork調用processUpdateQueue的時候,會被處理。這樣保證了getDerivedStateFromError
和componentDidCatch
的調用,然後產生新的state,這個state表示這次錯誤的狀態。
錯誤邊界是類組件,在beginWork階段會執行finishClassComponent
,如果判斷組件有DidCapture,會卸載掉它所有的子節點,然後重新渲染新的子節點,這些子節點有可能是經過錯誤處理渲染的備用UI。
示例代碼來自React錯誤邊界介紹
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下壹次渲染能夠顯示降級後的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 妳同樣可以將錯誤日誌上報給服務器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 妳可以自定義降級後的 UI 並渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
對於上述情況來說,壹旦ErrorBoundary的子樹中有某個節點發生了錯誤,組件中的getDerivedStateFromError
和 componentDidCatch
就會被觸發,
此時的備用UI就是:
<h1>Something went wrong.</h1>
上面的錯誤處理我們用圖來梳理壹下,假設<Example/>
具有錯誤處理的能力。
1 App
|
|
2 <Example/>
/
/
3 ---> <List/>--->span
/
/
4 p ----> 'text'
/
/
5 h1
1.如果<List/>
更新出錯,那麽首先throwException
會給它打上Incomplete的effectTag,然後以它的父節點為起點向上找到可以處理錯誤的節點。
2.找到了<Example/>
,它可以處理錯誤,給他打上ShouldCapture的effectTag(做記號),創建錯誤的update,將getDerivedStateFromError
放入payload,componentDidCatch
放入callback。
,入隊<Example/>
的updateQueue。
3.從<List/>
開始直接completeUnitOfWork
。由於它有Incomplete,所以會走unwindWork
,然後給它的父節點<Example/>
打上Incomplete,unwindWork
發現它不是剛剛做記號的錯誤邊界,
繼續向上completeUnitOfWork
。
4.<Example/>
有Incomplete,進入unwindWork
,而它恰恰是剛剛做過記號的錯誤邊界節點,去掉ShouldCapture打上DidCapture,將workInProgress的指針指向<Example/>
5.<Example/>
重新進入beginWork處理updateQueue,調和子節點(卸載掉原有的子節點,渲染備用UI)。
我們可以看出來,React的錯誤邊界的概念其實是對可以處理錯誤的組件重新進行更新。錯誤邊界只能捕獲它子樹的錯誤,而不能捕獲到它自己的錯誤,自己的錯誤要靠它上面的錯誤邊界來捕獲。
我想這是由於出錯的組件已經無法再渲染出它的子樹,也就意味著它不能渲染出備用UI,所以即使它捕獲到了自己的錯誤也於事無補。
這壹點在throwException
函數中有體現,是從它的父節點開始向上找錯誤邊界:
// 從當前節點的父節點開始向上找
let workInProgress = returnFiber;
do {
...
} while (workInProgress !== null);
回到completeWork,它在整體的錯誤處理中做的事情就是對錯誤邊界內的節點進行處理:
以上我們只是分析了壹般場景下的錯誤處理,實際上在任務掛起(Suspense)時,也會走錯誤處理的邏輯,因為此時throw的錯誤值是個thenable對象,具體會在分析suspense時詳細解釋。
workInProgress節點的completeWork階段主要做的事情再來回顧壹下:
雖然用了不少的篇幅去講錯誤處理,但是仍然需要重點關註正常節點的處理過程。completeWork階段處在beginWork之後,commit之前,起到的是壹個承上啟下的作用。它接收到的是經過diff後的fiber節點,然後他自己要將DOM節點和effectList都準備好。因為commit階段是不能被打斷的,所以充分準備有利於commit階段做更少的工作。
壹旦workInProgress樹的所有節點都完成complete,則說明workInProgress樹已經構建完成,所有的更新工作已經做完,接下來這棵樹會進入commit階段,從下壹篇文章開始,我們會分析commit階段的各個過程。